// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/file_utils.dart';
import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart';
import 'package:git/git.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import 'mocks.dart';
import 'util.dart';

void main() {
  group('FirebaseTestLabCommand', () {
    late MockPlatform mockPlatform;
    late Directory packagesDir;
    late CommandRunner<void> runner;
    late RecordingProcessRunner processRunner;
    late RecordingProcessRunner gitProcessRunner;

    setUp(() {
      mockPlatform = MockPlatform();
      final GitDir gitDir;
      (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
          configureBaseCommandMocks(platform: mockPlatform);
      final command = FirebaseTestLabCommand(
        packagesDir,
        processRunner: processRunner,
        platform: mockPlatform,
        gitDir: gitDir,
      );

      runner = CommandRunner<void>(
        'firebase_test_lab_command',
        'Test for $FirebaseTestLabCommand',
      );
      runner.addCommand(command);
    });

    void writeJavaTestFile(
      RepositoryPackage plugin,
      String relativeFilePath, {
      String runnerClass = 'FlutterTestRunner',
    }) {
      childFileWithSubcomponents(
        plugin.directory,
        p.posix.split(relativeFilePath),
      ).writeAsStringSync('''
@DartIntegrationTest
@RunWith($runnerClass.class)
public class MainActivityTest {
  @Rule
  public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
}
''');
    }

    test('fails if gcloud auth fails', () async {
      processRunner.mockProcessesForExecutable['gcloud'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['auth']),
      ];

      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--service-key=/path/to/key',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Unable to activate gcloud account.'),
        ]),
      );
    });

    test('retries gcloud set', () async {
      processRunner.mockProcessesForExecutable['gcloud'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(), <String>['auth']),
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['config']),
      ];

      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--service-key=/path/to/key',
        '--project=a-project',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains(
            'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.',
          ),
        ]),
      );
    });

    test('only runs gcloud configuration once', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin1 = createFakePlugin(
        'plugin1',
        packagesDir,
        extraFiles: <String>[
          'test/plugin_test.dart',
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin1, javaTestFileRelativePath);
      final RepositoryPackage plugin2 = createFakePlugin(
        'plugin2',
        packagesDir,
        extraFiles: <String>[
          'test/plugin_test.dart',
          'example/integration_test/bar_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin2, javaTestFileRelativePath);

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--project=a-project',
        '--service-key=/path/to/key',
        '--device',
        'model=redfin,version=30',
        '--device',
        'model=seoul,version=26',
        '--test-run-id',
        'testRunId',
        '--build-id',
        'buildId',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin1'),
          contains('Firebase project configured.'),
          contains('Testing example/integration_test/foo_test.dart...'),
          contains('Running for plugin2'),
          contains('Testing example/integration_test/bar_test.dart...'),
        ]),
      );

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>[
            'build',
            'apk',
            '--debug',
            '--config-only',
          ], plugin1.getExamples().first.directory.path),
          ProcessCall(
            'gcloud',
            'auth activate-service-account --key-file=/path/to/key'.split(' '),
            null,
          ),
          ProcessCall(
            'gcloud',
            'config set project a-project'.split(' '),
            null,
          ),
          ProcessCall(
            '/packages/plugin1/example/android/gradlew',
            'app:assembleAndroidTest -Pverbose=true'.split(' '),
            '/packages/plugin1/example/android',
          ),
          ProcessCall(
            '/packages/plugin1/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart'
                .split(' '),
            '/packages/plugin1/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin1/example',
          ),
          ProcessCall('flutter', const <String>[
            'build',
            'apk',
            '--debug',
            '--config-only',
          ], plugin2.getExamples().first.directory.path),
          ProcessCall(
            '/packages/plugin2/example/android/gradlew',
            'app:assembleAndroidTest -Pverbose=true'.split(' '),
            '/packages/plugin2/example/android',
          ),
          ProcessCall(
            '/packages/plugin2/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart'
                .split(' '),
            '/packages/plugin2/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin2/example',
          ),
        ]),
      );
    });

    test('runs integration tests', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'test/plugin_test.dart',
          'example/integration_test/bar_test.dart',
          'example/integration_test/foo_test.dart',
          'example/integration_test/should_not_run.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
        '--device',
        'model=seoul,version=26',
        '--test-run-id',
        'testRunId',
        '--build-id',
        'buildId',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains('Testing example/integration_test/bar_test.dart...'),
          contains('Testing example/integration_test/foo_test.dart...'),
        ]),
      );
      expect(output, isNot(contains('test/plugin_test.dart')));
      expect(
        output,
        isNot(contains('example/integration_test/should_not_run.dart')),
      );

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>[
            'build',
            'apk',
            '--debug',
            '--config-only',
          ], plugin.getExamples().first.directory.path),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleAndroidTest -Pverbose=true'.split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/bar_test.dart'
                .split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin/example',
          ),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart'
                .split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin/example',
          ),
        ]),
      );
    });

    test('runs for all examples', () async {
      const examples = <String>['example1', 'example2'];
      const javaTestFileExampleRelativePath =
          'android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        examples: examples,
        extraFiles: <String>[
          for (final String example in examples) ...<String>[
            'example/$example/integration_test/a_test.dart',
            'example/$example/android/gradlew',
            'example/$example/$javaTestFileExampleRelativePath',
          ],
        ],
      );
      for (final example in examples) {
        writeJavaTestFile(
          plugin,
          'example/$example/$javaTestFileExampleRelativePath',
        );
      }

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
        '--device',
        'model=seoul,version=26',
        '--test-run-id',
        'testRunId',
        '--build-id',
        'buildId',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Testing example/example1/integration_test/a_test.dart...'),
          contains('Testing example/example2/integration_test/a_test.dart...'),
        ]),
      );

      expect(
        processRunner.recordedCalls,
        containsAll(<ProcessCall>[
          ProcessCall(
            '/packages/plugin/example/example1/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example1/integration_test/a_test.dart'
                .split(' '),
            '/packages/plugin/example/example1/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin/example/example1',
          ),
          ProcessCall(
            '/packages/plugin/example/example2/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example2/integration_test/a_test.dart'
                .split(' '),
            '/packages/plugin/example/example2/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26'
                .split(' '),
            '/packages/plugin/example/example2',
          ),
        ]),
      );
    });

    test('fails if a test fails twice', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/bar_test.dart',
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      processRunner.mockProcessesForExecutable['gcloud'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>[
          'firebase',
          'test',
        ]), // integration test #1
        FakeProcessInfo(MockProcess(exitCode: 1), <String>[
          'firebase',
          'test',
        ]), // integration test #1 retry
        FakeProcessInfo(MockProcess(), <String>[
          'firebase',
          'test',
        ]), // integration test #2
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Testing example/integration_test/bar_test.dart...'),
          contains('Testing example/integration_test/foo_test.dart...'),
          contains(
            'plugin:\n'
            '    example/integration_test/bar_test.dart failed tests',
          ),
        ]),
      );
    });

    test(
      'passes with warning if a test fails once, then passes on retry',
      () async {
        const javaTestFileRelativePath =
            'example/android/app/src/androidTest/MainActivityTest.java';
        final RepositoryPackage plugin = createFakePlugin(
          'plugin',
          packagesDir,
          extraFiles: <String>[
            'example/integration_test/bar_test.dart',
            'example/integration_test/foo_test.dart',
            'example/android/gradlew',
            javaTestFileRelativePath,
          ],
        );
        writeJavaTestFile(plugin, javaTestFileRelativePath);

        processRunner.mockProcessesForExecutable['gcloud'] = <FakeProcessInfo>[
          FakeProcessInfo(MockProcess(exitCode: 1), <String>[
            'firebase',
            'test',
          ]), // integration test #1
          FakeProcessInfo(MockProcess(), <String>[
            'firebase',
            'test',
          ]), // integration test #1 retry
          FakeProcessInfo(MockProcess(), <String>[
            'firebase',
            'test',
          ]), // integration test #2
        ];

        final List<String> output = await runCapturingPrint(runner, <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Testing example/integration_test/bar_test.dart...'),
            contains('bar_test.dart failed on attempt 1. Retrying...'),
            contains('Testing example/integration_test/foo_test.dart...'),
            contains('Ran for 1 package(s) (1 with warnings)'),
          ]),
        );
      },
    );

    test('fails for plugins with no androidTest directory', () async {
      createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
        ],
      );

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains('No androidTest directory found.'),
          contains('The following packages had errors:'),
          contains(
            'plugin:\n'
            '    No tests ran (use --exclude if this is intentional).',
          ),
        ]),
      );
    });

    test(
      'skips for non-plugin packages with no androidTest directory',
      () async {
        createFakePackage(
          'a_package',
          packagesDir,
          extraFiles: <String>[
            'example/integration_test/foo_test.dart',
            'example/android/gradlew',
          ],
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for a_package'),
            contains('No androidTest directory found.'),
            contains('No examples support Android.'),
            contains('Skipped 1 package'),
          ]),
        );
      },
    );

    test('fails for packages with no integration test files', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains('No integration tests were run'),
          contains('The following packages had errors:'),
          contains(
            'plugin:\n'
            '    No tests ran (use --exclude if this is intentional).',
          ),
        ]),
      );
    });

    test('fails for packages with no integration_test runner', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'test/plugin_test.dart',
          'example/integration_test/bar_test.dart',
          'example/integration_test/foo_test.dart',
          'example/integration_test/should_not_run.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      // Use the wrong @RunWith annotation.
      writeJavaTestFile(
        plugin,
        javaTestFileRelativePath,
        runnerClass: 'AndroidJUnit4.class',
      );

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains(
            'No integration_test runner found. '
            'See the integration_test package README for setup instructions.',
          ),
          contains(
            'plugin:\n'
            '    No integration_test runner.',
          ),
        ]),
      );
    });

    test('supports kotlin implementation of integration_test runner', () async {
      const kotlinTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.kt';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'test/plugin_test.dart',
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          kotlinTestFileRelativePath,
        ],
      );

      // Kotlin equivalent of the test runner
      childFileWithSubcomponents(
        plugin.directory,
        p.posix.split(kotlinTestFileRelativePath),
      ).writeAsStringSync('''
@DartIntegrationTest
@RunWith(FlutterTestRunner::class)
class MainActivityTest {
  @JvmField @Rule var rule = ActivityTestRule(MainActivity::class.java)
}
''');

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains('Testing example/integration_test/foo_test.dart...'),
          contains('Ran for 1 package'),
        ]),
      );
    });

    test('skips packages with no android directory', () async {
      createFakePackage(
        'package',
        packagesDir,
        extraFiles: <String>['example/integration_test/foo_test.dart'],
      );

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for package'),
          contains('No examples support Android'),
        ]),
      );
      expect(
        output,
        isNot(contains('Testing example/integration_test/foo_test.dart...')),
      );

      expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
    });

    test('builds if gradlew is missing', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      final List<String> output = await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
        '--test-run-id',
        'testRunId',
        '--build-id',
        'buildId',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Running for plugin'),
          contains('Running flutter build apk...'),
          contains('Testing example/integration_test/foo_test.dart...'),
        ]),
      );

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall(
            'flutter',
            'build apk --debug --config-only'.split(' '),
            plugin.getExamples().first.directory.path,
          ),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleAndroidTest -Pverbose=true'.split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart'
                .split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30'
                .split(' '),
            '/packages/plugin/example',
          ),
        ]),
      );
    });

    test('fails if building to generate gradlew fails', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['build']),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[contains('Unable to build example apk')]),
      );
    });

    test('fails if assembleAndroidTest fails', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      final String gradlewPath = plugin
          .getExamples()
          .first
          .platformDirectory(FlutterPlatform.android)
          .childFile('gradlew')
          .path;
      processRunner.mockProcessesForExecutable[gradlewPath] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>[
          'app:assembleAndroidTest',
        ]),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Unable to assemble androidTest'),
        ]),
      );
    });

    test('fails if assembleDebug fails', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      final String gradlewPath = plugin
          .getExamples()
          .first
          .platformDirectory(FlutterPlatform.android)
          .childFile('gradlew')
          .path;
      processRunner.mockProcessesForExecutable[gradlewPath] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(), <String>['app:assembleAndroidTest']),
        FakeProcessInfo(MockProcess(exitCode: 1), <String>[
          'app:assembleDebug',
        ]),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Could not build example/integration_test/foo_test.dart'),
          contains('The following packages had errors:'),
          contains(
            '  plugin:\n'
            '    example/integration_test/foo_test.dart failed to build',
          ),
        ]),
      );
    });

    test('experimental flag', () async {
      const javaTestFileRelativePath =
          'example/android/app/src/androidTest/MainActivityTest.java';
      final RepositoryPackage plugin = createFakePlugin(
        'plugin',
        packagesDir,
        extraFiles: <String>[
          'example/integration_test/foo_test.dart',
          'example/android/gradlew',
          javaTestFileRelativePath,
        ],
      );
      writeJavaTestFile(plugin, javaTestFileRelativePath);

      await runCapturingPrint(runner, <String>[
        'firebase-test-lab',
        '--results-bucket=a_bucket',
        '--device',
        'model=redfin,version=30',
        '--test-run-id',
        'testRunId',
        '--build-id',
        'buildId',
        '--enable-experiment=exp1',
      ]);

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>[
            'build',
            'apk',
            '--debug',
            '--config-only',
            '--enable-experiment=exp1',
          ], plugin.getExamples().first.directory.path),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1'
                .split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            '/packages/plugin/example/android/gradlew',
            'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1'
                .split(' '),
            '/packages/plugin/example/android',
          ),
          ProcessCall(
            'gcloud',
            'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://a_bucket --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30'
                .split(' '),
            '/packages/plugin/example',
          ),
        ]),
      );
    });

    group('file filtering', () {
      const files = <String>[
        'pubspec.yaml',
        'foo.dart',
        'foo.java',
        'foo.kt',
        'foo.m',
        'foo.swift',
        'foo.cc',
        'foo.cpp',
        'foo.h',
      ];
      for (final file in files) {
        test('runs command for changes to $file', () async {
          createFakePackage('package_a', packagesDir);

          gitProcessRunner.mockProcessesForExecutable['git-diff'] =
              <FakeProcessInfo>[
                FakeProcessInfo(
                  MockProcess(
                    stdout:
                        '''
packages/package_a/$file
''',
                  ),
                ),
              ];

          final List<String> output = await runCapturingPrint(runner, <String>[
            'firebase-test-lab',
            '--results-bucket=a_bucket',
            '--device',
            'model=redfin,version=30',
          ]);

          expect(
            output,
            containsAllInOrder(<Matcher>[contains('Running for package_a')]),
          );
        });
      }

      test('skips commands if all files should be ignored', () async {
        createFakePackage('package_a', packagesDir);

        gitProcessRunner.mockProcessesForExecutable['git-diff'] =
            <FakeProcessInfo>[
              FakeProcessInfo(
                MockProcess(
                  stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''',
                ),
              ),
            ];

        final List<String> output = await runCapturingPrint(runner, <String>[
          'firebase-test-lab',
          '--results-bucket=a_bucket',
          '--device',
          'model=redfin,version=30',
        ]);

        expect(
          output,
          isNot(
            containsAllInOrder(<Matcher>[contains('Running for package_a')]),
          ),
        );
        expect(
          output,
          containsAllInOrder(<Matcher>[contains('SKIPPING ALL PACKAGES')]),
        );
      });
    });
  });
}
